/*
 * ENUM Discoverer. ENUM Demonstration software, demonstrating the potential of ENUM.
 * 
 * Copyright (C) 2009 SIDN and ISOC.nl
 * 
 * This file is part of ENUM Discoverer.
 *
 * ENUM Discoverer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ENUM Discoverer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with ENUM Discoverer.  If not, see <http://www.gnu.org/licenses/>.
 */

package nl.sidn.enumdiscoverer.contacts;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map.Entry;

import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import nl.sidn.enumdiscoverer.EnumRecord;
import nl.sidn.enumdiscoverer.LogConfig;
import nl.sidn.enumdiscoverer.Utils;
import nl.sidn.enumdiscoverer.contacts.dao.Coords;
import nl.sidn.enumdiscoverer.contacts.dao.PersonDao;

/**
 * Aggregates a contact's profile from multiple sources referenced in an ENUM lookup.
 * The order and preference fields are used to resolve conflicting data from multiple sources.
 * @author	Grant Patterson (grant.patterson@catalyst.net.nz)
 */
public class ContactProfileAggregator {

	static final String TAG = LogConfig.getLogTag(ContactProfileAggregator.class);
	// whether DEBUG level logging is enabled (whether globally, or explicitly for this log tag)
	static final boolean DEBUG = LogConfig.isDebug(TAG);
	// whether VERBOSE level logging is enabled
	static final boolean VERBOSE = LogConfig.VERBOSE;
	
	/*
	 * Events passed to Handler
	 */
	/** A name has been added and it is of higher priority than previous names. TYPE: String */
	public static final int NAME_CHANGED    = 40000;
	/** The main (highest priority) address changed. TYPE: String */
	public static final int ADDRESS_CHANGED = NAME_CHANGED + 1;
	/** The main (highest priority) organisation changed. TYPE: String */
	public static final int ORG_CHANGED     = ADDRESS_CHANGED + 1;
	/** A photo has been added and it is of higher priority than previous photos. TYPE: String */
	public static final int PHOTO_CHANGED   = ORG_CHANGED + 1;
	/** A new link icon was added. TYPE: int, String */
	public static final int WEB_LINK_ADDED  = PHOTO_CHANGED + 1;
	public static final int LOCATION_CHANGED = WEB_LINK_ADDED + 1;
	public static final int PUBLIC_KEY_SET = LOCATION_CHANGED + 1;
	public static final int NICKNAME_CHANGED = PUBLIC_KEY_SET + 1;
	public static final int BIRTHDAY_CHANGED = NICKNAME_CHANGED + 1;
	public static final int SIP_ADDRESS_SET = BIRTHDAY_CHANGED + 1;	
	public static final int STATUS_SET = SIP_ADDRESS_SET + 1;	
	
	// The DAO to use to update Contacts
	/*package*/ final PersonDao mContact;
	// The Handler to pass events to
	/*package*/ final Handler mHandler;
	
	// Web links (social networking, etc). One per service
	private HashMap<Integer, EnumRecord> mWebLinks = new HashMap<Integer, EnumRecord>();
	
	
	// The property aggregators that need to be flushed when flush() is called
	private final ArrayList<SingleValuePropertyAggregator<?>> mAggregatorsToFlush
			= new ArrayList<SingleValuePropertyAggregator<?>>();
	
	private final SingleValuePropertyAggregator<String> mNameAggregator;
	private final SingleValuePropertyAggregator<String> mPhotoAggregator;
	private final SingleValuePropertyAggregator<Coords> mLocationAggregator;
	private final SingleValuePropertyAggregator<Date>   mBirthdayAggregator;
	private final SingleValuePropertyAggregator<String> mNicknameAggregator;
	private final SingleValuePropertyAggregator<String> mPublicKeyAggregator;
	private final SingleValuePropertyAggregator<String> mSipAddressAggregator;
	private final SingleValuePropertyAggregator<String> mStatusAggregator;
	
	private final MultiValuePropertyAggregator<String> mAddressAggregator;
	private final MultiValuePropertyAggregator<String> mOrganisationAggregator;
	
	
	public ContactProfileAggregator(PersonDao contactDao) {
		this(contactDao, null);
	}
	
	public ContactProfileAggregator(PersonDao contactDao, Handler eventHandler) {
		mContact = contactDao;
		mHandler = eventHandler;
		
		/*
		 * Single-value property aggregators
		 */
		
		PropertyWriter<String> nameWriter = new PropertyWriter<String>() {
			@Override
			public void writeProperty(String value) {
				mContact.setName(value);
			}
		};
		mNameAggregator = createSingleValuePropertyAggregator("name", NAME_CHANGED, nameWriter);
		
		PropertyWriter<String> photoWriter = new PropertyWriter<String>() {
			@Override
			public void writeProperty(String value) {
				writePhoto(value);
			}
		};
		mPhotoAggregator = createSingleValuePropertyAggregator("photo", PHOTO_CHANGED, photoWriter);
		
		PropertyWriter<Coords> locWriter = new PropertyWriter<Coords>() {
			@Override
			public void writeProperty(Coords value) {
				mContact.setLocation(value);
			}
		};
		mLocationAggregator = new LocationAggregator("location", LOCATION_CHANGED, locWriter);
		mAggregatorsToFlush.add(mLocationAggregator);
		
		PropertyWriter<Date> bDayWriter = new PropertyWriter<Date>() {
			@Override
			public void writeProperty(Date value) {
				mContact.setDateOfBirth(value);
			}
		};
		mBirthdayAggregator = createSingleValuePropertyAggregator("birthday", BIRTHDAY_CHANGED, bDayWriter);
		
		PropertyWriter<String> nicknameWriter = new PropertyWriter<String>() {
			@Override
			public void writeProperty(String value) {
				// TODO: write nickname to contacts
			}
		};
		mNicknameAggregator = createSingleValuePropertyAggregator("nickname", NICKNAME_CHANGED, nicknameWriter);
		
		PropertyWriter<String> keyWriter = new PropertyWriter<String>() {
			@Override
			public void writeProperty(String value) {
				mContact.setPublicKey(value);
			}
		};
		mPublicKeyAggregator = createSingleValuePropertyAggregator("public key", PUBLIC_KEY_SET, keyWriter);
		
		PropertyWriter<String> sipWriter = new PropertyWriter<String>() {
			@Override
			public void writeProperty(String value) {
				mContact.setSipAddress(value);
			}
		};
		mSipAddressAggregator = createSingleValuePropertyAggregator("SIP address", SIP_ADDRESS_SET, sipWriter);
		
		PropertyWriter<String> statusWriter = new PropertyWriter<String>() {
			@Override
			public void writeProperty(String value) {
				mContact.setStatus(value);
			}
		};
		mStatusAggregator = createSingleValuePropertyAggregator("status", STATUS_SET, statusWriter);
		
		
		/*
		 * Multi-value property aggregators
		 */
		
		PropertyWriter<String> addressWriter = new PropertyWriter<String>() {
			@Override
			public void writeProperty(String value) {
				mContact.addPostalAddress(value);
			}
		};
		mAddressAggregator = new MultiValuePropertyAggregator<String>(
				"postal address", ADDRESS_CHANGED, addressWriter);
		
		PropertyWriter<String> orgWriter = new PropertyWriter<String>() {
			@Override
			public void writeProperty(String value) {
				mContact.addOrganisation(value);
			}
		};
		mOrganisationAggregator = new MultiValuePropertyAggregator<String>(
				"organisation", ORG_CHANGED, orgWriter);
	}
	
	private <T> SingleValuePropertyAggregator<T> createSingleValuePropertyAggregator(
			String propName, int onUpdateEvent, PropertyWriter<T> propWriter) {
		
		SingleValuePropertyAggregator<T> agg = new SingleValuePropertyAggregator<T>(
				propName, onUpdateEvent, propWriter);
		
		mAggregatorsToFlush.add(agg);
		
		return agg;
	}
	
	/**
	 * writes all cached aggregated contact data to the underlying PersonDao 
	 */
	public void flush() {
		
		// flush all the property aggregators that might need it
		ArrayList<SingleValuePropertyAggregator<?>> propAggs = mAggregatorsToFlush;
		int size = propAggs.size();
		for (int i = 0; i < size; i++) {
			propAggs.get(i).flushValue();
		}
		
		writeWebLinks();
	}
	
	/**
	 * Adds a name to be considered for writing to the contact. Only a single
	 * name is actually associated with a contact, determined by the order/preference
	 * field of the ENUM record used to discover the name.
	 * An event is generated if this is the first name added, or origin has higher priority
	 * than previously added names
	 * precondition: origin not null
	 * @param name The contact's name
	 * @param origin The record that was used to discover the name
	 * @throws NullPointerException if origin is null
	 */
	public void addName(String name, EnumRecord origin) {
		
		mNameAggregator.offerValue(name, origin);
	}
	
	public void addWebLink(int service, EnumRecord enumRecord) {
		
		EnumRecord current = mWebLinks.get(service);
		
		if (   current == null                       // no EnumRecord for this service yet
		    || enumRecord.compareTo(current) < 0) {  // the new enumRecord takes precedence
			
			mWebLinks.put(service, enumRecord);
			
			if (mHandler != null) {
				Message msg = mHandler.obtainMessage(WEB_LINK_ADDED, enumRecord.getUrl());
				msg.arg1 = service;
				mHandler.sendMessage(msg);
			}
			
		} // else retain current
	}
	
	/**
	 * Write aggregated web links to Contacts
	 */
	private void writeWebLinks() {
		
		HashMap<Integer, String> links = new HashMap<Integer, String>();
		
		for (Entry<Integer, EnumRecord> entry : mWebLinks.entrySet()) {
			links.put(entry.getKey(), entry.getValue().getUrl());
		}
		
		mContact.setWebLinks(links);
	}
	
	/**
	 * Adds a photo to be considered for writing to the contact. Only a single
	 * photo is actually associated with a contact, determined by the order/preference
	 * field of the ENUM record used to discover the photo.
	 * An event is generated if this is the first photo added, or origin has higher priority
	 * than previously added photos.
	 * precondition: origin not null
	 * @param url The URL a photo
	 * @param origin The record that was used to discover the photo
	 * @throws NullPointerException if origin is null
	 */
	public void addPhoto(String url, EnumRecord origin) {
		
		mPhotoAggregator.offerValue(url, origin);
	}
	
	/**
	 * Adds an address to the contact. This is passed to the DAO immediately.
	 * An event is generated only if this is the first address added, or origin has higher priority
	 * than previously added addresses
	 * precondition: origin not null
	 * @param address The address to add to contact information
	 * @param origin The record that was used to discover the address
	 */
	public void addAddress(String address, EnumRecord origin) {
		
		mAddressAggregator.offerValue(address, origin);
	}
	
	/**
	 * Adds an organisation to the contact. This is passed to the DAO immediately.
	 * An event is generated only if this is the first organisation added, or origin has higher priority
	 * than previously added organisations
	 * precondition: origin not null
	 * @param organisation The organisation to add to contact information
	 * @param origin The record that was used to discover the address
	 */
	public void addOrganisation(String organisation, EnumRecord origin) {
		
		mOrganisationAggregator.offerValue(organisation, origin);
	}
	
	/**
	 * 
	 * precondition: origin not null
	 * @param key A public key for the contact
	 */
	public void addPublicKey(String key, EnumRecord origin) {
		
		mPublicKeyAggregator.offerValue(key, origin);
	}
	
	/**
	 * 
	 * precondition: origin not null
	 * @param key A SIP address for the contact
	 */
	public void addSIPAddress(String address, EnumRecord origin) {
		
		mSipAddressAggregator.offerValue(address, origin);
	}
	
	/**
	 * 
	 * precondition: origin not null
	 * @param key A status for the contact (last tweet, hyves 'onmymind', etc)
	 */
	public void addStatus(String status, EnumRecord origin) {
		
		mStatusAggregator.offerValue(status, origin);
	}
	
	/**
	 * Writes a photo to the underlying DAO. The URL is used to retrieve the actual
	 * image, which is then written to contacts
	 */
	/*package*/ void writePhoto(String photoUrl) {
		
		if (photoUrl == null) return;  // nothing to do
		
		// overwrite existing photo
		byte[] photo = Utils.fetchImageFromUrl(photoUrl);
		if (photo != null) {
			mContact.setPhoto(photo);
		}
	}
	
	/**
	 * Adds a phone number to the underlying PersonDao. This is passed to the DAO
	 * immediately
	 * @param number The phone number
	 */
	public void addPhoneNumber(String number) {
		mContact.addPhoneNumber(number);
	}
	
	public void addNickName(String nickName, EnumRecord origin) {
		
		mNicknameAggregator.offerValue(nickName, origin);
	}
	
	public void addDateOfBirth(Date birthday, EnumRecord origin) {
		
		mBirthdayAggregator.offerValue(birthday, origin);
	}
	
	/**
	 * Adds an email address to the underlying PersonDao. This is passed to the DAO
	 * immediately.
	 * @param address The email address URL
	 */
	public void addEmailAddress(String address) {
		mContact.addEmailAddress(address);
	}
	
	/**
	 * Adds an instant messaging URL to the underlying PersonDao. This is passed to the DAO
	 * immediately.
	 * @param address The instant messaging URL
	 * @param protocol A protocol defined in ImProtocols
	 */
	public void addInstantMessagingUrl(String url, int protocol) {
		mContact.addInstantMessagingUrl(url, protocol);
	}
	
	/**
	 * Adds a location to be considered for writing to the contact. Only a single
	 * location is actually associated with a contact, determined by the order/preference
	 * field of the ENUM record used to discover the location.
	 * An event is generated if this is the first location added, or origin has higher priority
	 * than previously added locations.
	 * precondition: origin not null
	 * @param latitudeE6 The latitude * 1000000
	 * @param longitudeE6 The longitude * 1000000
	 * @param origin The record that was used to discover the location
	 */
	public void addLocation(int latitudeE6, int longitudeE6, EnumRecord origin) {
		
		mLocationAggregator.offerValue(new Coords(latitudeE6, longitudeE6), origin);
	}
	
	/**
	 * Fires an event for mHandler if mHandler has been set. Otherwise, does nothing
	 * @param event The event to fire
	 * @param obj An Object to pass with the event
	 */
	/*package*/ void fireEvent(int event, Object obj) {
		if (mHandler != null) {
			Message msg = mHandler.obtainMessage(event, obj);
			boolean result = mHandler.sendMessage(msg);
			if (!result) Log.i(TAG, "fireEvent '" + event + "' failed.");
		}
	}
	
	
	private class SingleValuePropertyAggregator<T> {
		
		protected final String mPropName;
		protected final int mOnUpdateEvent;
		private final PropertyWriter<? super T> mPropertyWriter;
		
		private T mValue;
		private EnumRecord mValueOrigin;
		
		public SingleValuePropertyAggregator(String propertyName, int onUpdateEvent,
				PropertyWriter<? super T> propertyWriter) {
			
			mPropName = propertyName;
			mOnUpdateEvent = onUpdateEvent;
			mPropertyWriter = propertyWriter;
		}
		
		/**
		 * Offers a value to be considered for writing using this property aggregator's
		 * PropertyWriter. Only a single values is actually written, determined
		 * by the order/preference field of the ENUM record used to discover the value.
		 * An event is generated if this is the first value offered, or origin has higher priority
		 * than previously added values
		 * precondition: origin not null
		 * @param value The value being offered
		 * @param origin The record that was used to discover the value
		 * @throws NullPointerException if origin is null
		 */
		public void offerValue(T value, EnumRecord origin) {
			
			if (value == null) return;
			
			if (VERBOSE) Log.v(TAG, "Adding " + mPropName + ": " + value + " from " + origin.getUrl());
			
			// Keep the value with the lowest order/preference
			// If there is no value yet, then keep this one
			
			if (mValue == null || origin.compareTo(mValueOrigin) < 0) {
				
				mValue = value;
				mValueOrigin = origin;
				
				if (VERBOSE) Log.v(TAG, mPropName + " changed. Firing event");
				onUpdate(value, origin.getUrl());
				
			} else if (VERBOSE) {
				String str = String.format("The %s %s isn't of higher priority than current %s. Keeping %s",
						mPropName, value, mPropName, mValue);
				Log.v(TAG, str);
			}
		}
		
		/**
		 * Override to change the default event-firing behaviour
		 * @param value
		 * @param url
		 */
		protected void onUpdate(T value, String url) {
			fireEvent(mOnUpdateEvent, value);
		}
		
		public void flushValue() {
			
			if (mValue == null) return;  // nothing to do
			
			if (VERBOSE) Log.v(TAG, "Writing " + mPropName + ": " + mValue);
			
			mPropertyWriter.writeProperty(mValue);
		}
	}
	
	private class MultiValuePropertyAggregator<T> {
		
		private final String mPropName;
		private final int mOnUpdateEvent;
		private final PropertyWriter<? super T> mPropertyWriter;
		
		// The ENUM record that the highest-priority. Used for notifying event listeners
		private EnumRecord mMainOrigin;
		
		public MultiValuePropertyAggregator(String propertyName, int onUpdateEvent,
				PropertyWriter<? super T> propertyWriter) {
			
			mPropName = propertyName;
			mOnUpdateEvent = onUpdateEvent;
			mPropertyWriter = propertyWriter;
		}
		
		/**
		 * Writes value immediately using this property aggregator's PropertyWriter.
		 * An event will be fired if the highest-priority value has changed.
		 * @param value The value to write
		 * @param origin The ENUM record used to obtain this value
		 */
		public void offerValue(T value, EnumRecord origin) {
			
			if (value == null) return;
			
			if (VERBOSE) Log.v(TAG, "Adding " + mPropName + ": " + value + " from " + origin.getUrl());
			
			// if there is no handler registered then there is no need to keep track of priorities
			if (mHandler != null) {
				if (mMainOrigin == null ||
						origin.compareTo(mMainOrigin) < 0) {
					
					mMainOrigin = origin;
					
					if (VERBOSE) Log.v(TAG, "Main " + mPropName + " changed. Firing event");
					fireEvent(mOnUpdateEvent, value);
				}
			}
			
			mPropertyWriter.writeProperty(value);
		}
	}
	
	private class LocationAggregator extends SingleValuePropertyAggregator<Coords> {

		public LocationAggregator(String propertyName, int onUpdateEvent,
				PropertyWriter<? super Coords> propertyWriter) {
			super(propertyName, onUpdateEvent, propertyWriter);
		}

		@Override
		protected void onUpdate(Coords value, String url) {
			if (mHandler != null) {
				Uri uri = Uri.parse(url);
				Message msg = mHandler.obtainMessage(
						mOnUpdateEvent, value.getLatitudeE6(), value.getLongitudeE6(), uri);
				mHandler.sendMessage(msg);
			}
		}
	}
	
	private interface PropertyWriter<T> {
		void writeProperty(T value);
	}
}
